{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "/mnt/workspace/mdy/miniforge/envs/mdy/lib/python3.10/site-packages/_distutils_hack/__init__.py:53: UserWarning: Reliance on distutils from stdlib is deprecated. Users must rely on setuptools to provide the distutils module. Avoid importing distutils or import setuptools first, and avoid setting SETUPTOOLS_USE_DISTUTILS=stdlib. Register concerns at https://github.com/pypa/setuptools/issues/new?template=distutils-deprecation.yml\n",
      "  warnings.warn(\n"
     ]
    }
   ],
   "source": [
    "import torch\n",
    "import triton\n",
    "import triton.language as tl"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- 也许你看了offical的矩阵乘法的例子，可能有点懵逼，我将用更简单的办法进行实现，性能相当\n",
    "- 每个代码定义完后，我都用tensor写出了计算顺序，方便你理解\n",
    "- 当dim大的时候，还是内置的快"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "\n",
    "def get_autotune_config_offical():\n",
    "    return [\n",
    "        triton.Config({'BLOCK_SIZE_M': 128, 'BLOCK_SIZE_N': 256, 'BLOCK_SIZE_K': 64, 'GROUP_SIZE_M': 8}, num_stages=3,\n",
    "                      num_warps=8),\n",
    "        triton.Config({'BLOCK_SIZE_M': 64, 'BLOCK_SIZE_N': 256, 'BLOCK_SIZE_K': 32, 'GROUP_SIZE_M': 8}, num_stages=4,\n",
    "                      num_warps=4),\n",
    "        triton.Config({'BLOCK_SIZE_M': 128, 'BLOCK_SIZE_N': 128, 'BLOCK_SIZE_K': 32, 'GROUP_SIZE_M': 8}, num_stages=4,\n",
    "                      num_warps=4),\n",
    "        triton.Config({'BLOCK_SIZE_M': 128, 'BLOCK_SIZE_N': 64, 'BLOCK_SIZE_K': 32, 'GROUP_SIZE_M': 8}, num_stages=4,\n",
    "                      num_warps=4),\n",
    "        triton.Config({'BLOCK_SIZE_M': 64, 'BLOCK_SIZE_N': 128, 'BLOCK_SIZE_K': 32, 'GROUP_SIZE_M': 8}, num_stages=4,\n",
    "                      num_warps=4),\n",
    "        triton.Config({'BLOCK_SIZE_M': 128, 'BLOCK_SIZE_N': 32, 'BLOCK_SIZE_K': 32, 'GROUP_SIZE_M': 8}, num_stages=4,\n",
    "                      num_warps=4),\n",
    "        triton.Config({'BLOCK_SIZE_M': 64, 'BLOCK_SIZE_N': 32, 'BLOCK_SIZE_K': 32, 'GROUP_SIZE_M': 8}, num_stages=5,\n",
    "                      num_warps=2),\n",
    "        triton.Config({'BLOCK_SIZE_M': 32, 'BLOCK_SIZE_N': 64, 'BLOCK_SIZE_K': 32, 'GROUP_SIZE_M': 8}, num_stages=5,\n",
    "                      num_warps=2),\n",
    "    ]\n",
    "\n",
    "\n",
    "def get_autotune_config_my():\n",
    "    return [\n",
    "        triton.Config({'BLOCK_SIZE_M': 128, 'BLOCK_SIZE_N': 256, 'BLOCK_SIZE_K': 64}, num_stages=3,\n",
    "                      num_warps=8),\n",
    "        triton.Config({'BLOCK_SIZE_M': 64, 'BLOCK_SIZE_N': 256, 'BLOCK_SIZE_K': 32}, num_stages=4,\n",
    "                      num_warps=4),\n",
    "        triton.Config({'BLOCK_SIZE_M': 128, 'BLOCK_SIZE_N': 128, 'BLOCK_SIZE_K': 32}, num_stages=4,\n",
    "                      num_warps=4),\n",
    "        triton.Config({'BLOCK_SIZE_M': 128, 'BLOCK_SIZE_N': 64, 'BLOCK_SIZE_K': 32}, num_stages=4,\n",
    "                      num_warps=4),\n",
    "        triton.Config({'BLOCK_SIZE_M': 64, 'BLOCK_SIZE_N': 128, 'BLOCK_SIZE_K': 32}, num_stages=4,\n",
    "                      num_warps=4),\n",
    "        triton.Config({'BLOCK_SIZE_M': 128, 'BLOCK_SIZE_N': 32, 'BLOCK_SIZE_K': 32}, num_stages=4,\n",
    "                      num_warps=4),\n",
    "        triton.Config({'BLOCK_SIZE_M': 64, 'BLOCK_SIZE_N': 32, 'BLOCK_SIZE_K': 32}, num_stages=5,\n",
    "                      num_warps=2),\n",
    "        triton.Config({'BLOCK_SIZE_M': 32, 'BLOCK_SIZE_N': 64, 'BLOCK_SIZE_K': 32}, num_stages=5,\n",
    "                      num_warps=2),\n",
    "    ]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# offical"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "tensor([[[ 0,  2,  4,  6,  8, 10, 12, 14],\n",
      "         [ 1,  3,  5,  7,  9, 11, 13, 15]],\n",
      "\n",
      "        [[16, 18, 20, 22, 24, 26, 28, 30],\n",
      "         [17, 19, 21, 23, 25, 27, 29, 31]],\n",
      "\n",
      "        [[32, 34, 36, 38, 40, 42, 44, 46],\n",
      "         [33, 35, 37, 39, 41, 43, 45, 47]],\n",
      "\n",
      "        [[48, 50, 52, 54, 56, 58, 60, 62],\n",
      "         [49, 51, 53, 55, 57, 59, 61, 63]]])\n"
     ]
    }
   ],
   "source": [
    "@triton.autotune(\n",
    "    configs=get_autotune_config_offical(),\n",
    "    key=['M', 'N', 'K'],\n",
    ")\n",
    "@triton.jit\n",
    "def offical_matmul_kernel(\n",
    "        # Pointers to matrices\n",
    "        a_ptr, b_ptr, c_ptr,\n",
    "        # Matrix dimensions\n",
    "        M, N, K,\n",
    "        # The stride variables represent how much to increase the ptr by when moving by 1\n",
    "        # element in a particular dimension. E.g. `stride_am` is how much to increase `a_ptr`\n",
    "        # by to get the element one row down (A has M rows).\n",
    "        stride_am, stride_ak,  #\n",
    "        stride_bk, stride_bn,  #\n",
    "        stride_cm, stride_cn,\n",
    "        # Meta-parameters\n",
    "        BLOCK_SIZE_M: tl.constexpr, BLOCK_SIZE_N: tl.constexpr, BLOCK_SIZE_K: tl.constexpr,  #\n",
    "        GROUP_SIZE_M: tl.constexpr,  #\n",
    "        ACTIVATION: tl.constexpr  #\n",
    "):\n",
    "    \"\"\"Kernel for computing the matmul C = A x B.\n",
    "    A has shape (M, K), B has shape (K, N) and C has shape (M, N)\n",
    "    \"\"\"\n",
    "    # -----------------------------------------------------------\n",
    "    # Map program ids `pid` to the block of C it should compute.\n",
    "    # This is done in a grouped ordering to promote L2 data reuse.\n",
    "    # See above `L2 Cache Optimizations` section for details.\n",
    "    pid = tl.program_id(axis=0)\n",
    "    num_pid_m = tl.cdiv(M, BLOCK_SIZE_M)\n",
    "    num_pid_n = tl.cdiv(N, BLOCK_SIZE_N)\n",
    "    num_pid_in_group = GROUP_SIZE_M * num_pid_n\n",
    "    group_id = pid // num_pid_in_group\n",
    "    first_pid_m = group_id * GROUP_SIZE_M\n",
    "    group_size_m = min(num_pid_m - first_pid_m, GROUP_SIZE_M)\n",
    "    pid_m = first_pid_m + ((pid % num_pid_in_group) % group_size_m)\n",
    "    pid_n = (pid % num_pid_in_group) // group_size_m\n",
    "\n",
    "    # ----------------------------------------------------------\n",
    "    # Create pointers for the first blocks of A and B.\n",
    "    # We will advance this pointer as we move in the K direction\n",
    "    # and accumulate\n",
    "    # `a_ptrs` is a block of [BLOCK_SIZE_M, BLOCK_SIZE_K] pointers\n",
    "    # `b_ptrs` is a block of [BLOCK_SIZE_K, BLOCK_SIZE_N] pointers\n",
    "    # See above `Pointer Arithmetic` section for details\n",
    "    offs_am = (pid_m * BLOCK_SIZE_M + tl.arange(0, BLOCK_SIZE_M)) % M\n",
    "    offs_bn = (pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N)) % N\n",
    "    offs_k = tl.arange(0, BLOCK_SIZE_K)\n",
    "    a_ptrs = a_ptr + (offs_am[:, None] * stride_am + offs_k[None, :] * stride_ak)\n",
    "    b_ptrs = b_ptr + (offs_k[:, None] * stride_bk + offs_bn[None, :] * stride_bn)\n",
    "\n",
    "    # -----------------------------------------------------------\n",
    "    # Iterate to compute a block of the C matrix.\n",
    "    # We accumulate into a `[BLOCK_SIZE_M, BLOCK_SIZE_N]` block\n",
    "    # of fp32 values for higher accuracy.\n",
    "    # `accumulator` will be converted back to fp16 after the loop.\n",
    "    accumulator = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_N), dtype=tl.float32)\n",
    "    for k in range(0, tl.cdiv(K, BLOCK_SIZE_K)):\n",
    "        # Load the next block of A and B, generate a mask by checking the K dimension.\n",
    "        # If it is out of bounds, set it to 0.\n",
    "        a = tl.load(a_ptrs, mask=offs_k[None, :] < K - k * BLOCK_SIZE_K, other=0.0)\n",
    "        b = tl.load(b_ptrs, mask=offs_k[:, None] < K - k * BLOCK_SIZE_K, other=0.0)\n",
    "        # We accumulate along the K dimension.\n",
    "        accumulator = tl.dot(a, b, accumulator)\n",
    "        # Advance the ptrs to the next K block.\n",
    "        a_ptrs += BLOCK_SIZE_K * stride_ak\n",
    "        b_ptrs += BLOCK_SIZE_K * stride_bk\n",
    "    # You can fuse arbitrary activation functions here\n",
    "    # while the accumulator is still in FP32!\n",
    "    if ACTIVATION == \"leaky_relu\":\n",
    "        accumulator = leaky_relu(accumulator)\n",
    "    c = accumulator.to(tl.float16)\n",
    "\n",
    "    # -----------------------------------------------------------\n",
    "    # Write back the block of the output matrix C with masks.\n",
    "    offs_cm = pid_m * BLOCK_SIZE_M + tl.arange(0, BLOCK_SIZE_M)\n",
    "    offs_cn = pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N)\n",
    "    c_ptrs = c_ptr + stride_cm * offs_cm[:, None] + stride_cn * offs_cn[None, :]\n",
    "    c_mask = (offs_cm[:, None] < M) & (offs_cn[None, :] < N)\n",
    "    tl.store(c_ptrs, c, mask=c_mask)\n",
    "\n",
    "\n",
    "# We can fuse `leaky_relu` by providing it as an `ACTIVATION` meta-parameter in `matmul_kernel`.\n",
    "@triton.jit\n",
    "def leaky_relu(x):\n",
    "    return tl.where(x >= 0, x, 0.01 * x)\n",
    "\n",
    "# %%\n",
    "# We can now create a convenience wrapper function that only takes two input tensors,\n",
    "# and (1) checks any shape constraint; (2) allocates the output; (3) launches the above kernel.\n",
    "\n",
    "\n",
    "def offical_matmul(a, b, activation=\"\"):\n",
    "    # Check constraints.\n",
    "    assert a.shape[1] == b.shape[0], \"Incompatible dimensions\"\n",
    "    assert a.is_contiguous(), \"Matrix A must be contiguous\"\n",
    "    M, K = a.shape\n",
    "    K, N = b.shape\n",
    "    # Allocates output.\n",
    "    c = torch.empty((M, N), device=a.device, dtype=torch.float16)\n",
    "    # 1D launch kernel where each block gets its own program.\n",
    "    BLOCK_SIZE_M = min(128, triton.next_power_of_2(M))\n",
    "    BLOCK_SIZE_N = min(128, triton.next_power_of_2(N))\n",
    "    BLOCK_SIZE_K = 128\n",
    "    GROUP_SIZE_M = 8\n",
    "    num_warps = 8\n",
    "    num_stages = 2\n",
    "    grid = lambda META: (triton.cdiv(M, META['BLOCK_SIZE_M']) * triton.cdiv(N, META['BLOCK_SIZE_N']), )\n",
    "    offical_matmul_kernel[grid](\n",
    "        a, b, c,  #\n",
    "        M, N, K,  #\n",
    "        a.stride(0), a.stride(1),  #\n",
    "        b.stride(0), b.stride(1),  #\n",
    "        c.stride(0), c.stride(1),  #\n",
    "        ACTIVATION=activation,  #\n",
    "    )\n",
    "    return c\n",
    "# 输出结果计算顺序示意图，最外层是super_group，GROUP_SIZE_M=2, 每个super_group都是结果的一部分，block的计算顺序和数字一致\n",
    "# 他说这么做能提高L2缓存的命中率，我也不太懂硬件知识\n",
    "grid = torch.arange(64).view(4, 8, 2).transpose(-1, -2)\n",
    "print(grid)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# my"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "tensor([[ 0,  1,  2,  3,  4,  5,  6,  7],\n",
      "        [ 8,  9, 10, 11, 12, 13, 14, 15],\n",
      "        [16, 17, 18, 19, 20, 21, 22, 23],\n",
      "        [24, 25, 26, 27, 28, 29, 30, 31],\n",
      "        [32, 33, 34, 35, 36, 37, 38, 39],\n",
      "        [40, 41, 42, 43, 44, 45, 46, 47],\n",
      "        [48, 49, 50, 51, 52, 53, 54, 55],\n",
      "        [56, 57, 58, 59, 60, 61, 62, 63]])\n"
     ]
    }
   ],
   "source": [
    "@triton.autotune(\n",
    "    configs=get_autotune_config_my(),\n",
    "    key=['M', 'N', 'K'],\n",
    ")\n",
    "@triton.jit\n",
    "def my_matmul_kernel(\n",
    "        # Pointers to matrices\n",
    "        a_ptr, b_ptr, c_ptr,\n",
    "        # Matrix dimensions\n",
    "        M, N, K,\n",
    "        # The stride variables represent how much to increase the ptr by when moving by 1\n",
    "        # element in a particular dimension. E.g. `stride_am` is how much to increase `a_ptr`\n",
    "        # by to get the element one row down (A has M rows).\n",
    "        stride_am, stride_ak,  #\n",
    "        stride_bk, stride_bn,  #\n",
    "        stride_cm, stride_cn,\n",
    "        # Meta-parameters\n",
    "        BLOCK_SIZE_M: tl.constexpr, BLOCK_SIZE_N: tl.constexpr, BLOCK_SIZE_K: tl.constexpr,  #\n",
    "        ACTIVATION: tl.constexpr  #\n",
    "):\n",
    "    \"\"\"Kernel for computing the matmul C = A x B.\n",
    "    A has shape (M, K), B has shape (K, N) and C has shape (M, N)\n",
    "    \"\"\"\n",
    "    pid = tl.program_id(axis=0)\n",
    "    num_block_n = tl.cdiv(N, BLOCK_SIZE_N)\n",
    "    pid_m = pid // num_block_n\n",
    "    pid_n = pid % num_block_n\n",
    "    # a_ptrs += pid_m * stride_am * BLOCK_SIZE_M\n",
    "    # n_offset = pid_n * stride_bn * BLOCK_SIZE_N\n",
    "\n",
    "    a_block_ptrs = tl.make_block_ptr(\n",
    "        base=a_ptr,\n",
    "        shape=(M, K),\n",
    "        strides=(stride_am, stride_ak),\n",
    "        offsets=(pid_m * BLOCK_SIZE_M, 0),\n",
    "        block_shape=(BLOCK_SIZE_M, BLOCK_SIZE_K),\n",
    "        order=(1,0)\n",
    "    )\n",
    "\n",
    "    b_block_ptrs = tl.make_block_ptr(\n",
    "        base=b_ptr,\n",
    "        shape=(K, N),\n",
    "        strides=(stride_bk, stride_bn),\n",
    "        offsets=(0, pid_n*BLOCK_SIZE_N),\n",
    "        block_shape=(BLOCK_SIZE_K, BLOCK_SIZE_N),\n",
    "        order=(1,0)\n",
    "    )\n",
    "\n",
    "    c_block_ptrs = tl.make_block_ptr(\n",
    "        base=c_ptr,\n",
    "        shape=(M, N),\n",
    "        strides=(stride_cm, stride_cn),\n",
    "        offsets=(pid_m*BLOCK_SIZE_M, pid_n*BLOCK_SIZE_N),\n",
    "        block_shape=(BLOCK_SIZE_M, BLOCK_SIZE_N),\n",
    "        order=(1,0)\n",
    "    )\n",
    "\n",
    "\n",
    "\n",
    "    accumulator = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_N), dtype=tl.float32)\n",
    "    for k in range(0, tl.cdiv(K, BLOCK_SIZE_K)):\n",
    "        # Load the next block of A and B, generate a mask by checking the K dimension.\n",
    "        # If it is out of bounds, set it to 0.\n",
    "        a = tl.load(a_block_ptrs, boundary_check=(0,), padding_option='zero')\n",
    "        b = tl.load(b_block_ptrs) # 理论上b不会溢出，相当于权重w，dim都是2的指数倍或者BLOCK_SIZE的整数倍\n",
    "        # We accumulate along the K dimension.\n",
    "        accumulator = tl.dot(a, b, accumulator)\n",
    "        # Advance the ptrs to the next K block.\n",
    "        a_block_ptrs = tl.advance(a_block_ptrs, offsets=(0, BLOCK_SIZE_K))\n",
    "        b_block_ptrs = tl.advance(b_block_ptrs, offsets=(BLOCK_SIZE_K, 0))\n",
    "    # You can fuse arbitrary activation functions here\n",
    "    # while the accumulator is still in FP32!\n",
    "    if ACTIVATION == \"leaky_relu\":\n",
    "        accumulator = leaky_relu(accumulator)\n",
    "    c = accumulator.to(tl.float16)\n",
    "\n",
    "    tl.store(c_block_ptrs, c) # M是bs * seq_len， 可能会溢出\n",
    "\n",
    "@triton.jit\n",
    "def leaky_relu(x):\n",
    "    return tl.where(x >= 0, x, 0.01 * x)\n",
    "\n",
    "# 我实现的这个函数没有什么泛化性，只适用于模型训练里的矩阵乘\n",
    "# 矩阵a的维度m无所谓， 但是b的n和k这两个都要求是2的指数倍，或者32、64、128的倍数\n",
    "# 因为kernel里直接读取的block（tl.make_block_ptrs），block只能防止一个维度溢出（需要mask， boundary_check），也就是m\n",
    "def my_matmul(a, b, activation=\"\"):\n",
    "    # Check constraints.\n",
    "    input_shape = a.shape\n",
    "    a = a.view(-1, input_shape[-1])\n",
    "    M, K = a.shape\n",
    "    K, N = b.shape\n",
    "    # Allocates output.\n",
    "    c = torch.empty((M, N), device=a.device, dtype=torch.float16)\n",
    "    # 1D launch kernel where each block gets its own program.\n",
    "    BLOCK_SIZE_M = min(128, triton.next_power_of_2(M))\n",
    "    BLOCK_SIZE_N = min(128, triton.next_power_of_2(N))\n",
    "    BLOCK_SIZE_K = 32\n",
    "    num_warps = 4\n",
    "    num_stages = 4\n",
    "    grid = lambda META: (triton.cdiv(M, META['BLOCK_SIZE_M']) * triton.cdiv(N, META['BLOCK_SIZE_N']), )\n",
    "    my_matmul_kernel[grid](\n",
    "        a, b, c,  #\n",
    "        M, N, K,  #\n",
    "        a.stride(0), a.stride(1),  #\n",
    "        b.stride(0), b.stride(1),  #\n",
    "        c.stride(0), c.stride(1),  #\n",
    "        # BLOCK_SIZE_M, BLOCK_SIZE_N, BLOCK_SIZE_K,\n",
    "        ACTIVATION=activation,  #\n",
    "        # num_warps=num_warps, num_stages=num_stages,\n",
    "    )\n",
    "    return c.view(*input_shape[:-1],-1)\n",
    "# 输出结果直接按着顺序算\n",
    "grid = torch.arange(64).view(8, 8)\n",
    "print(grid)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [],
   "source": [
    "m,n,k = 1024, 768*4, 768\n",
    "dtype = torch.float16\n",
    "device = 'cuda'\n",
    "x = torch.randn(m, k).to(device).to(dtype)\n",
    "f = torch.nn.Linear(k, n).to(device).to(dtype)\n",
    "w = f.weight.data"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "True"
      ]
     },
     "execution_count": 7,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "y_offical = offical_matmul(x, w.T)\n",
    "y_my = my_matmul(x, w.T)\n",
    "torch.allclose(y_offical, y_my)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "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UpARERERu6UgIyIiInYr389+bRgGYE4HLiIiIvYh8/d25u/x68n3QSYuLg6AYsWK2bgSERERuVVxcXH4+vped7vF+K+oY+cyMjI4efIk3t7eWCyWf22PjY2lWLFiHDt2DB8fHxtUmPN0jfYvv18f6BrzC12j/csr12cYBnFxcYSFheHgcP2WMPn+joyDgwNFixb9z/18fHzy5Q/klXSN9i+/Xx/oGvMLXaP9ywvXd6M7MZnU2FdERETsloKMiIiI2K0CH2RcXV0ZNWoUrq6uti4lx+ga7V9+vz7QNeYXukb7Z2/Xl+8b+4qIiEj+VeDvyIiIiIj9UpARERERu6UgIyIiInZLQUZERETsVoEOMp9++iklSpTAzc2NevXqsX79eluXdNNee+01LBZLtleFChWyticlJTFo0CACAwPx8vKia9euREVFZTvG0aNHad++PR4eHhQuXJjnnnuOtLS03L6ULCtXrqRjx46EhYVhsViYO3dutu2GYfDqq68SGhqKu7s7LVq0YN++fdn2OX/+PL169cLHxwc/Pz/69u1LfHx8tn22bt1K48aNcXNzo1ixYowbNy6nLw347+vr06fPv77TNm3aZNsnL18fwJgxY6hTpw7e3t4ULlyYzp07s2fPnmz7WOtnc/ny5dx11124urpSpkwZpkyZktOXd1PX17Rp0399jwMGDMi2T169PoDPPvuMatWqZQ2GFhERwcKFC7O22/P3l+m/rtHev8NrGTt2LBaLhaFDh2atyw/fJQBGATVz5kzDxcXFmDRpkrFjxw6jX79+hp+fnxEVFWXr0m7KqFGjjMqVKxunTp3Kep05cyZr+4ABA4xixYoZS5YsMTZu3GjUr1/faNCgQdb2tLQ0o0qVKkaLFi2MzZs3G7/++qtRqFAhY+TIkba4HMMwDOPXX381XnrpJWPOnDkGYPz000/Zto8dO9bw9fU15s6da/zzzz/Gvffea5QsWdJITEzM2qdNmzZG9erVjbVr1xqrVq0yypQpYzzwwANZ22NiYozg4GCjV69exvbt240ZM2YY7u7uxhdffGHz6+vdu7fRpk2bbN/p+fPns+2Tl6/PMAyjdevWxuTJk43t27cbW7ZsMdq1a2eEh4cb8fHxWftY42fz4MGDhoeHhzFs2DBj586dxscff2w4OjoaixYtsvn13X333Ua/fv2yfY8xMTF2cX2G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",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "offical vs my:\n",
      "         n    offical         my      torch\n",
      "0    128.0   8.905570   8.145688   8.856210\n",
      "1    256.0   9.342350   9.133943   9.271216\n",
      "2    384.0  10.807302   9.954507   9.115842\n",
      "3    512.0  11.142408  11.565274  11.825675\n",
      "4    640.0  12.895500  11.961600  11.783441\n",
      "5    768.0  13.837923  13.549923  12.817350\n",
      "6    896.0  15.079319  14.445755  13.509927\n",
      "7   1024.0  15.209383  15.184253  13.598277\n",
      "8   1152.0  16.835313  16.320840  14.477041\n",
      "9   1280.0  17.700652  17.686266  15.020251\n",
      "10  1408.0  18.296732  18.667372  15.682429\n",
      "11  1536.0  20.211935  19.936901  16.886471\n",
      "12  1664.0  21.170620  20.699000  17.666280\n",
      "13  1792.0  22.354862  21.856967  17.761998\n",
      "14  1920.0  23.803499  23.918767  18.781940\n",
      "15  2048.0  25.691915  24.700576  19.564312\n",
      "16  2176.0  26.533974  25.780518  21.014445\n",
      "17  2304.0  28.694805  27.739082  22.728514\n",
      "18  2432.0  30.309903  29.281052  23.950132\n",
      "19  2560.0  31.794854  30.955140  24.324523\n",
      "20  2688.0  33.507664  33.361319  26.812334\n",
      "21  2816.0  36.412783  35.536580  32.269884\n",
      "22  2944.0  37.598297  37.000027  34.012899\n",
      "23  3072.0  39.120190  38.317956  35.667349\n",
      "24  3200.0  41.073866  41.080635  37.602473\n",
      "25  3328.0  43.291826  42.593472  38.894825\n",
      "26  3456.0  45.256842  44.070609  41.133221\n",
      "27  3584.0  52.233767  50.983489  48.412275\n",
      "28  3712.0  53.276580  52.927189  42.576607\n",
      "29  3840.0  55.928368  55.819102  46.034370\n",
      "30  3968.0  57.816781  56.928653  46.141393\n",
      "31  4096.0  58.771566  58.032718  46.754565\n"
     ]
    }
   ],
   "source": [
    "torch.cuda.empty_cache()\n",
    "@triton.testing.perf_report(\n",
    "    triton.testing.Benchmark(\n",
    "        x_names=['n'],  # argument names to use as an x-axis for the plot\n",
    "        x_vals=[128 * i for i in range(1, 32+1, 1)],  # different possible values for `x_name`\n",
    "        line_arg='provider',  # argument name whose value corresponds to a different line in the plot\n",
    "        line_vals=['offical', 'my', 'torch'],  # possible values for `line_arg``\n",
    "        line_names=[\n",
    "            \"offical\",\n",
    "            \"my\",\n",
    "            'torch',\n",
    "        ],  # label name for the lines\n",
    "        styles=[('blue', '-'), ('green', '-'), ('orange', '-')],  # line styles\n",
    "        ylabel=\"ms\",  # label name for the y-axis\n",
    "        plot_name=\"offical vs my\",  # name for the plot. Used also as a file name for saving the plot.\n",
    "        args={'m': 128, 'k': 4096}\n",
    "    ))\n",
    "def benchmark(m, n, k, provider):\n",
    "    device = torch.device('cuda')\n",
    "    dtype = torch.float16\n",
    "    # m,n,k = 1024, 768*4, 768\n",
    "    dtype = torch.float16\n",
    "    device = 'cuda'\n",
    "    x = torch.randn(128, n).to(device).to(dtype)\n",
    "    f = torch.nn.Linear(n, n).to(device).to(dtype)\n",
    "    w = f.weight.data\n",
    "    \n",
    "    stream = torch.cuda.Stream()\n",
    "    torch.cuda.set_stream(stream)\n",
    "    if provider == 'offical':\n",
    "        ms = triton.testing.do_bench(lambda: offical_matmul(x, w.T))\n",
    "\n",
    "    if provider == 'my':\n",
    "        ms = triton.testing.do_bench(lambda: my_matmul(x, w.T))\n",
    "\n",
    "    if provider == 'torch':\n",
    "        ms = triton.testing.do_bench(lambda: torch.matmul(x, w.T))\n",
    "\n",
    "    return ms * 1e3\n",
    "benchmark.run(show_plots=True, print_data=True)\n",
    "# 其实两者(offical vs my)性能差异不大, 如果想快速实现，表示清楚，直接按顺序计算即可\n",
    "# dim变大之后，貌似还是torch内置的矩阵乘法更优秀\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.13"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
